import logging
import re
from bbkb.KnowledgeBase import KnowledgeBase as kb

log = logging.getLogger(__name__)


class Query(object):
    def __init__(self, match_pattern):
        self.pattern = re.compile(match_pattern)

    def _match(self, query_string):
        return self.pattern.match(query_string)

    def matches(self, query_string):
        found = self._match(query_string)
        return found is not None

    def _return_unique(self, raw_items, term):
        items = set()
        for item in raw_items:
            base = item.split('~')[0]
            if base.startswith(term):
                items.add(base)
        return items

    def do_query(self):
        raise "Abstract Method"


class ItemQuery(Query):
    def __init__(self):
        super(ItemQuery, self).__init__('show (?P<item>\w+)')
        
    def do_query(self, query_string):
        m = super(ItemQuery, self)._match(query_string)
        if m:
            terms = {'item':m.group('item')}
            raw_items = kb._findstart(terms['item'])
            return super(ItemQuery, self)._return_unique(raw_items, terms['item'])


class ItemFilterQuery(Query):
    def __init__(self):
        super(ItemFilterQuery, self).__init__(
            "show (?P<item>\w+) (with|where) (?P<attr>\w+) (?P<op>\w+) (?P<expr>\w+)")

    def do_query(self, query_string):
        m = super(ItemFilterQuery, self)._match(query_string)
        if m is None:
            return None
        log.debug('FilterQuery found: %s',m.groupdict())
        terms = m.groupdict()
        raw_items = kb._findstart(terms['item'])
        items = [item for item in raw_items if 
                Filter.filter(item, terms['op'], terms['attr'], terms['expr'])]
        return super(ItemFilterQuery, self)._return_unique(items, terms['item'])

class ItemKeywordQuery(Query):
    def __init__(self):
        super(ItemKeywordQuery, self).__init__('')

    def do_query(self, query_string):
        keywords = parse_keywords(query_string)
        primaryObject = None
        display_attributes = []
        attributes = []
        
        if keywords is None:
            return None
        
        log.debug('ItemKeywordQuery found: %s',keywords)

        #Get the primary object for the query
        for primaryObject in (keyword for keyword in keywords if keyword['type'] == 'primary_object'):
            break

        #Get display attributes
        for keyword in (keyword for keyword in keywords if keyword['type'] == 'display_attribute'):
            display_attributes.append(keyword)

        #Get filter attributes (includes operators and values)
        for keyword in (keyword for keyword in keywords if keyword['type'] == 'attribute'):
            attributes.append(keyword)

        #Get all potential items based on the primaryObject
        if primaryObject['key'] in kb.lookup_tables:
            #TODO: Potentially allow for filtering on lookups
            potential_results = kb.lookup_tables[primaryObject['key']]
            displayResults = {}
            displayResults['columns'] = []
            displayResults['columns'].append('id')
            displayResults['columns'].append('frequency')
            displayResults['columns'].append(primaryObject['key'])
            for result in potential_results:
                displayResults[result] = []
            for result in potential_results:
                displayResults[result].append(result)
                displayResults[result].append(potential_results[result]['frequency'])
                displayResults[result].append(potential_results[result]['value'])
            return displayResults

        else:
            potential_results = kb._findstart(primaryObject['key'])
        log.debug('%s potential results', len(potential_results))

        #Filter the raw items by each filter attribute in turn
        results = self._compound_filter(potential_results, attributes)
            
        results = super(ItemKeywordQuery, self)._return_unique(results, primaryObject['key'])

        #populate the key column information
        displayResults = {}
        displayResults['columns'] = []
        displayResults['columns'].append(primaryObject['key'])
        for result in results:
            key = result.split(':')[1]
            displayResults[key] = []
            displayResults[key].append(key)
            
        #Hydrate result data using display_attributes
        for attribute in display_attributes:
            log.debug('Collecting display data for "%s"', attribute['key'])
            displayResults['columns'].append(attribute['key'])
            for result in sorted(list(results)):
                key = result.split(':')[1]
                relevantResults = []

                #find all data for each result item, for each given attribute
                for item in potential_results:
                    splits = item.split('~')
                    if len(splits) > 1 and splits[0] == result and splits[1].split(':')[0] == attribute['key']:
                        attKey = splits[1].split(':')[1]
                        attValue = kb.lookup_tables[attribute['key']][int(attKey)]['value']
                        relevantResults.append(attValue)
                        
                #map the display value(s) onto the result set
                displayResults[key].append(relevantResults)
        log.debug(displayResults)
        return displayResults
    
    def _compound_filter(self, data, attributes):
        new_attributes = list(attributes)

        #Remove the first attribute from the list for the next go around.
        # EOL if the list of attributes is empty
        if len(new_attributes) > 0:
            new_attributes.pop(0)

            data = [item for item in data if 
                    (Filter.filter(item, attributes[0]['operator'], attributes[0]['key'], attributes[0]['value']) and
                    item.split('~')[0] in self._compound_filter(data, new_attributes))]

        #At the tail end of recursion we will return the entire dataset, but only the base object ids
        newData = []
        for item in data:
            newData.append(item.split('~')[0])
        return newData

class Filter(object):
    @staticmethod
    def _todict(kbitem):
        d = {}
        parts = kbitem.split('~')
        for p in parts:
            name,value = p.split(':')
            d[name] = value
        return d

    @staticmethod
    def filter(item, operation, attr, contains_value):
        """Dispatch method"""
        if operation == 'contains':
            return Filter.contains(item, attr, contains_value)
        elif operation == 'greaterthan':
            return Filter.greater_than(item, attr, contains_value)
        elif operation == 'lessthan':
            return Filter.less_than(item, attr, contains_value)
        else:
            raise "Unknown operation: %s" % operation

    @staticmethod
    def contains(str_item, attr, contains_value):
        item_dict = Filter._todict(str_item)
        if attr in item_dict:
            lookup = kb.lookup('%s:%d' % (attr, int(item_dict[attr])))
            if str(lookup).lower().find(str(contains_value).lower()) > -1:
                return True
        return False
                
    @staticmethod
    def greater_than(str_item, attr, value):
        is_greater = False
        item_dict = Filter._todict(str_item)
        if attr in item_dict:
            lookup = kb.lookup('%s:%d' % (attr, int(item_dict[attr])))
            try:
                is_greater = int(lookup) > int(value)
            except:
                is_greater = lookup > value
        return is_greater

    @staticmethod
    def less_than(str_item, attr, value):
        comparison = False
        item_dict = Filter._todict(str_item)
        if attr in item_dict:
            lookup = kb.lookup('%s:%d' % (attr, int(item_dict[attr])))
            try:
                comparison = int(lookup) < int(value)
            except:
                comparison = lookup < value
        return comparison 



def query_knowledgebase(sentence):
    """Main query handler"""
    for handler in query_handlers:
        results = handler.do_query(sentence)
        if results is not None:
            return results
    return None

def parse_keywords(query_string):
    #Assumptions:
    # 1) Attributes discovered prior to the primary object are display attributes
    # 2) The first object encountered is the primary object, any other objects are regular objects
    # 3) All attributes are eventually followed by an operator
    # 4) The word immediately following an operator is a value
    
    objects = ['player', 'team']
    operators = {}
    keywords = []
    words = []
    primaryFound = False
    isValue = False
    operators['greaterthan'] = ['greaterthan', '>']
    operators['lessthan'] = ['lessthan', '<']
    operators['equals'] = ['equals', '==']
    operators['notequals'] = ['notequals', '!=']
    operators['contains'] = ['contains'] 

    #Split the query string into individual words.
    # Identify keywords from among those.
    words = query_string.split(' ')
    log.debug('Query words: %s', words)

    #iterate over each word
    for word in words:

        #if word is a lookup table, it's either an object or attribute
        if word in kb.lookup_tables or word in objects:

            #default
            type = 'attribute'

            #keyword represents an object. First encountered is primary.
            if word in objects:
                if primaryFound:
                    type = 'object'
                else:
                    type = 'primary_object'
                    primaryFound = True
                    
            #attributes specified before the primary object are display specifiers
            elif primaryFound == False:
                type = 'display_attribute'

            keywords.append({'key': word, 'type': type, 'value': None, 'operator': None})
    
        #If not a lookup table, could be an operator or attribute value
        else:
            #Attribute value (based on assumption that attribute values ALWAYS immediately follow operators)
            if isValue and len(keywords) > 0 and keywords[-1]['type'] == 'attribute' and keywords[-1]['value'] is None:
                lookupData = kb.lookup_tables[keywords[-1]['key']]
                isValue = False
                keywords[-1]['value'] = word
                    
            #Operator (based on string matching and presence of operator key in previous keyword)
            elif len(keywords) > 0 and keywords[-1]['type'] == 'attribute' and keywords[-1]['operator'] is None:
                word = word.lower()
                            
                for opKey in operators:
                    if word in operators[opKey]:
                        keywords[-1]['operator'] = opKey

                        #If we find an operator, we can assume the next word is a value
                        isValue = True
                        break

    #If we're done and don't have a primary object, the last display attribute becomes the primary object
    if primaryFound == False:
        for attribute in (k for k in keywords if k['type'] == 'display_attribute'):
            attribute['type'] = 'primary_object'
            break;
        
    log.debug('Query keywords: %s', keywords)
    return keywords

# Query handlers must be ordered from most specific
# to least specific
query_handlers = [ItemKeywordQuery(), ItemFilterQuery(), ItemQuery()]
